跳到主要内容

安全框架 Shiro 授权

大部分转载自 W3C School 的 Shiro 教程

Shiro 授权是什么?

授权,也叫访问控制,即在应用中控制谁能访问哪些资源(如访问页面/编辑数据/页面操作等)。在授权中需了解的几个关键对象:主体(Subject)、资源(Resource)、权限(Permission)、角色(Role)。

主体 :即访问应用的用户,在 Shiro 中使用 Subject 代表该用户。用户只有授权后才允许访问相应的资源。

资源:在应用中用户可以访问的 URL,比如访问 JSP 页面、查看/编辑某些数据、访问某个业务方法、打印文本等等都是资源。用户只要授权后才能访问。

权限:安全策略中的原子授权单位,通过权限我们可以表示在应用中用户有没有操作某个资源的权力。即权限表示在应用中用户能不能访问某个资源,如: 访问用户列表页面 查看/新增/修改/删除用户数据(即很多时候都是 CRUD(增查改删)式权限控制)打印文档等。如上可以看出,权限代表了用户有没有操作某个资源的权利,即反映在某个资源上的操作允不允许,不反映谁去执行这个操作。所以后续还需要把权限赋予给用户,即定义哪个用户允许在某个资源上做什么操作(权限),Shiro 不会去做这件事情,而是由实现人员提供。

Shiro 支持粗粒度权限(如用户模块的所有权限)和细粒度权限(操作某个用户的权限,即实例级别的)。

角色:角色代表了操作集合,可以理解为权限的集合,一般情况下我们会赋予用户角色而不是权限,即这样用户可以拥有一组权限,赋予权限时比较方便。典型的如:项目经理、技术总监、CTO、开发工程师等都是角色,不同的角色拥有一组不同的权限。

隐式角色:即直接通过角色来验证用户有没有操作权限,如在应用中 CTO、技术总监、开发工程师可以使用打印机,假设某天不允许开发工程师使用打印机,此时需要从应用中删除相应代码;再如在应用中 CTO、技术总监可以查看用户、查看权限;突然有一天不允许技术总监查看用户、查看权限了,需要在相关代码中把技术总监角色从判断逻辑中删除掉;即粒度是以角色为单位进行访问控制的,粒度较粗;如果进行修改可能造成多处代码修改。

显示角色:在程序中通过权限控制谁能访问某个资源,角色聚合一组权限集合;这样假设哪个角色不能访问某个资源,只需要从角色代表的权限集合中移除即可;无须修改多处代码;即粒度是以资源/实例为单位的;粒度较细。

授权方式

Shiro 支持三种方式的授权:

编程式:通过写 if/else 授权代码块完成:

Subject subject = SecurityUtils.getSubject();
if(subject.hasRole("admin")) {
//有权限
} else {
//无权限
}

注解式:通过在执行的 Java 方法上放置相应的注解完成:

@RequiresRoles("admin")
public void hello() {
//有权限
}

没有权限将抛出相应的异常;

JSP/GSP 标签:在 JSP/GSP 页面通过相应的标签完成:

<shiro:hasRole name="admin">
<!-- 有权限 -->
</shiro:hasRole>

授权流程

流程如下:

首先调用 Subject.isPermitted*/hasRole* 接口,其会委托给 SecurityManager,而 SecurityManager 接着会委托给 Authorizer;

Authorizer 是真正的授权者,如果我们调用如 isPermitted("user:view"),其首先会通过 PermissionResolver 把字符串转换成相应的 Permission 实例;

在进行授权之前,其会调用相应的 Realm 获取 Subject 相应的角色/权限用于匹配传入的角色/权限;

Authorizer 会判断 Realm 的角色/权限是否和传入的匹配,如果有多个 Realm,会委托给 ModularRealmAuthorizer 进行循环判断,如果匹配如 isPermitted*/hasRole* 会返回 true,否则返回 false 表示授权失败。

ModularRealmAuthorizer 进行多 Realm 匹配流程:

  • 首先检查相应的 Realm 是否实现了实现了 Authorizer;
  • 如果实现了 Authorizer,那么接着调用其相应的 isPermitted*/hasRole* 接口进行匹配;
  • 如果有一个 Realm 匹配那么将返回 true,否则返回 false。

如果 Realm 进行授权的话,应该继承 AuthorizingRealm,其流程是:

如果调用 hasRole*,则直接获取 AuthorizationInfo.getRoles() 与传入的角色比较即可;首先如果调用如 isPermitted("user:view"),首先通过 PermissionResolver 将权限字符串转换成相应的 Permission 实例,默认使用 WildcardPermissionResolver,即转换为通配符的 WildcardPermission;

通过 AuthorizationInfo.getObjectPermissions() 得到 Permission 实例集合;通过 AuthorizationInfo.getStringPermissions() 得到字符串集合并通过 PermissionResolver 解析为 Permission 实例;然后获取用户的角色,并通过 RolePermissionResolver 解析角色对应的权限集合(默认没有实现,可以自己提供);

接着调用 Permission.implies(Permission p) 逐个与传入的权限比较,如果有匹配的则返回 true,否则 false。

授权角色

隐式角色

基于角色的访问控制(隐式角色)

1、在 ini 配置文件配置用户拥有的角色(shiro-role.ini)

[users]
zhang=123,role1,role2
wang=123,role1

规则即:用户名=密码,角色1,角色2,如果需要在应用中判断用户是否有相应角色,就需要在相应的 Realm 中返回角色信息,也就是说 Shiro 不负责维护用户-角色信息,需要应用提供,Shiro 只是提供相应的接口方便验证,后续会介绍如何动态的获取用户角色。

2、测试用例

class RoleTest {
/**
* 抽象出统一的逻辑,以后直接调用就行了
*/
private Subject login(String configFile, String user, String password) {
DefaultSecurityManager securityManager = new DefaultSecurityManager();
IniRealm iniRealm = new IniRealm(configFile);
securityManager.setRealm(iniRealm);

SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(user, password);
subject.login(token);
return subject;
}

@Test
void testHasRole() {
Subject subject = login("classpath:shiro-role.ini", "zhang", "123");
//判断拥有角色:role1
Assertions.assertTrue(subject.hasRole("role1"));
//判断拥有角色:role1 and role2
Assertions.assertTrue(subject.hasAllRoles(Arrays.asList("role1", "role2")));
//判断拥有角色:role1 and role2 and !role3
boolean[] result = subject.hasRoles(Arrays.asList("role1", "role2", "role3"));
Assertions.assertEquals(true, result[0]);
Assertions.assertEquals(true, result[1]);
Assertions.assertEquals(false, result[2]);
}
}

Shiro 提供了 hasRole/hasRoles 用于判断用户是否拥有某个角色/某些权限;但是没有提供如 hashAnyRole 用于判断是否有某些权限中的某一个。

@Test
void testCheckRole() {
Subject subject = login("classpath:shiro-role.ini", "zhang", "123");
//断言拥有角色:role1
subject.checkRole("role1");
//断言拥有角色:role1 and role3 失败抛出异常
subject.checkRoles("role1", "role3");
}

Shiro 提供的 checkRole/checkRoles 和 hasRole/hasAllRoles 不同的地方是它在判断为假的情况下会抛出 UnauthorizedException 异常。

到此基于角色的访问控制(即隐式角色)就完成了,这种方式的缺点就是如果很多地方进行了角色判断,但是有一天不需要了那么就需要修改相应代码把所有相关的地方进行删除;这就是粗粒度造成的问题。

显示角色

基于资源的访问控制(显示角色)

1、在 ini 配置文件配置用户拥有的角色及角色-权限关系(shiro-permission.ini

[users]
zhang=123,role1,role2
wang=123,role1

[roles]
role1=user:create,user:update
role2=user:create,user:delete

规则:"用户名=密码,角色 1,角色 2" "角色=权限 1,权限 2",即首先根据用户名找到角色,然后根据角色再找到权限;即角色是权限集合;Shiro 同样不进行权限的维护,需要我们通过 Realm 返回相应的权限信息。只需要维护 “用户——角色” 之间的关系即可。

2、测试用例

class PermissionTest {
/**
* 抽象出统一的逻辑,以后直接调用就行了
*/
private Subject login(String configFile, String user, String password) {
DefaultSecurityManager securityManager = new DefaultSecurityManager();
IniRealm iniRealm = new IniRealm(configFile);
securityManager.setRealm(iniRealm);

SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(user, password);
subject.login(token);
return subject;
}

@Test
void testIsPermitted() {
Subject subject = login("classpath:shiro-permission.ini", "zhang", "123");
//判断拥有权限:user:create
Assertions.assertTrue(subject.isPermitted("user:create"));
//判断拥有权限:user:update and user:delete
Assertions.assertTrue(subject.isPermittedAll("user:update", "user:delete"));
//判断没有权限:user:view (返回 false
Assertions.assertTrue(subject.isPermitted("user:view"));
}
}

Shiro 提供了 isPermitted 和 isPermittedAll 用于判断用户是否拥有某个权限或所有权限,也没有提供如 isPermittedAny 用于判断拥有某一个权限的接口。

@Test
void testCheckPermission () {
Subject subject = login("classpath:shiro-permission.ini", "zhang", "123");
//断言拥有权限:user:create
subject.checkPermission("user:create");
//断言拥有权限:user:delete and user:update
subject.checkPermissions("user:delete", "user:update");
//断言拥有权限:user:view 失败抛出异常
subject.checkPermissions("user:view");
}

但是失败的情况下会抛出 UnauthorizedException 异常。

Permission

其默认支持通配符权限字符串,“:” 表示资源/操作/实例的分割;“,” 表示操作的分割;“*” 表示任意资源/操作/实例。

1、单个资源单个权限

subject.checkPermissions("system:user:update");

用户拥有资源 “system:user”“update” 权限。

2、单个资源多个权限

role41=system:user:update,system:user:delete

然后通过如下代码判断

subject.checkPermissions("system:user:update", "system:user:delete");

用户拥有资源 “system:user”“update”“delete” 权限。

如上可以简写成:ini 配置(表示角色4拥有 system:user 资源的 update 和 delete 权限)

role42="system:user:update,delete"

接着可以通过如下代码判断

subject.checkPermissions("system:user:update,delete");

通过 “system:user:update,delete” 验证 “system:user:update, system:user:delete” 是没问题的,但是反过来是规则不成立。

3、单个资源全部权限

ini 配置

role51="system:user:create,update,delete,view"

然后通过如下代码判断

subject.checkPermissions("system:user:create,update,delete,view");

用户拥有资源 “system:user” 的 “create”、“update”、“delete” 和 “view” 所有权限。如上可以简写成: ini 配置文件(表示角色 5 拥有 system:user 的所有权限)

role52=system:user:*

也可以简写为(推荐上边的写法):

role53=system:user

然后通过如下代码判断

subject.checkPermissions("system:user:*");
subject.checkPermissions("system:user");

通过 “system:user:*” 验证 “system:user:create,delete,update:view” 可以,但是反过来是不成立的。

4、所有资源单个权限

ini 配置

role61=*:view

然后通过如下代码判断

subject.checkPermissions("user:view");

用户拥有所有资源的 “view” 所有权限。假设判断的权限是 system:user:view,那么需要 “role5=::view” 这样写才行

5、实例级别的权限

1、单个实例单个权限

role71=user:view:1

对资源 user 的 1 实例拥有 view 权限。

然后通过如下代码判断

subject.checkPermissions("user:view:1");

2、单个实例多个权限

role72="user:update,delete:1"

对资源 user 的 1 实例拥有 update、delete 权限。

然后通过如下代码判断

subject.checkPermissions("user:delete,update:1");
subject.checkPermissions("user:update:1", "user:delete:1");

3、单个实例所有权限

role73=user:*:1

对资源 user 的 1 实例拥有所有权限。

subject.checkPermissions("user:update:1", "user:delete:1", "user:view:1");

4、所有实例单个权限

role74=user:auth:*
subject.checkPermissions("user:auth:1", "user:auth:2");

5、所有实例所有权限

role75=user:*:*
subject.checkPermissions("user:view:1", "user:auth:2");